--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit c1e196ed900200721cd6853b031384c2b42e09e2
Parents : e4a1b54
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-05T01:24:40-05:00
refactor(ci): remove Bunny Storage upload steps and associated script from release workflow
Changes
2 files changed, 0 insertions(+), 232 deletions(-)
Diff
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index bb99c294..76004411 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -1,9 +1,6 @@
# Single tagged-release pipeline: Linux release assets, Windows + macOS Electron
# builds, Flatpak, Android APKs (dev/master track tags via android-apk-tag.yml), SLSA
# provenance (generic generator: Linux, desktop, optional Android+Flatpak), optional cosign bundles, and one draft GitHub release.
-# Optional: same draft upload/ tree is mirrored to bunny.net Edge Storage for master/dev
-# tags when BUNNY_STORAGE_ACCESS_KEY is set (see draft job). -rc tags always use the
-# dev/ prefix; previous release folders under master/ and dev/ are pruned after each upload.
# One workflow run per tag keeps the release graph immutable.
#
# Pinned first-party actions (bump tag and SHA together when upgrading):
@@ -569,33 +566,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- with:
- fetch-depth: 0
-
- - name: Resolve tag release track
- id: release_track
- run: |
- set -euo pipefail
- git fetch --no-tags origin dev master
- sha="$(git rev-parse HEAD)"
- on_m=false
- on_d=false
- while IFS= read -r line; do
- ref="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
- case "$ref" in
- origin/master) on_m=true ;;
- origin/dev) on_d=true ;;
- esac
- done < <(git branch -r --contains "${sha}")
- if [[ "${on_m}" == true ]]; then
- track=master
- elif [[ "${on_d}" == true ]]; then
- track=dev
- else
- track=none
- fi
- echo "track=${track}" >> "${GITHUB_OUTPUT}"
- echo "Resolved tag ${GITHUB_REF_NAME} -> track=${track}"
- name: Download Linux release assets (x64)
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
@@ -692,29 +662,6 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: bash scripts/ci/github-draft-release-upload-assets.sh upload
- - name: Upload release assets to Bunny Storage
- if: >-
- !startsWith(github.ref_name, 'preview-') &&
- (steps.release_track.outputs.track == 'master' || steps.release_track.outputs.track == 'dev')
- env:
- BUNNY_STORAGE_ACCESS_KEY: ${{ secrets.BUNNY_STORAGE_ACCESS_KEY }}
- BUNNY_STORAGE_BASE_URL: ${{ vars.BUNNY_STORAGE_BASE_URL }}
- BUNNY_RELEASE_TRACK: ${{ steps.release_track.outputs.track }}
- run: |
- set -euo pipefail
- if [[ -z "${BUNNY_STORAGE_ACCESS_KEY:-}" ]]; then
- echo "::notice::Skipping Bunny Storage (set repository secret BUNNY_STORAGE_ACCESS_KEY to enable)."
- exit 0
- fi
- export BUNNY_STORAGE_BASE_URL="${BUNNY_STORAGE_BASE_URL:-https://la.storage.bunnycdn.com/meshchatx}"
- _ref_lc="${GITHUB_REF_NAME,,}"
- if [[ "${_ref_lc}" == *"-rc"* ]]; then
- export BUNNY_STORAGE_OBJECT_PREFIX="dev/${GITHUB_REF_NAME}"
- else
- export BUNNY_STORAGE_OBJECT_PREFIX="${BUNNY_RELEASE_TRACK}/${GITHUB_REF_NAME}"
- fi
- python3 scripts/ci/github-upload-bunny-storage-release-assets.py upload
-
release-artifact-gate:
name: Verify required tagged release artifacts
if: >-
diff --git a/scripts/ci/github-upload-bunny-storage-release-assets.py b/scripts/ci/github-upload-bunny-storage-release-assets.py
deleted file mode 100644
index 1fac40f8..00000000
--- a/scripts/ci/github-upload-bunny-storage-release-assets.py
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/usr/bin/env python3
-"""Upload a directory tree to bunny.net Edge Storage (HTTP PUT per object)."""
-
-from __future__ import annotations
-
-import hashlib
-import json
-import mimetypes
-import os
-import sys
-import time
-import urllib.error
-import urllib.request
-from pathlib import Path
-from urllib.parse import quote
-
-
-def encode_object_rel(rel: str) -> str:
- return "/".join(quote(part, safe="") for part in rel.split("/"))
-
-
-def mime_for(path: Path) -> str:
- if path.suffix.lower() == ".wasm":
- return "application/wasm"
- guessed, _enc = mimetypes.guess_type(path.name)
- return guessed or "application/octet-stream"
-
-
-def get_json(url: str, access_key: str, timeout: int = 120) -> object:
- req = urllib.request.Request(
- url,
- method="GET",
- headers={"AccessKey": access_key},
- )
- with urllib.request.urlopen(req, timeout=timeout) as resp:
- body = resp.read()
- return json.loads(body.decode("utf-8"))
-
-
-def delete_path(url: str, access_key: str, timeout: int = 120) -> None:
- req = urllib.request.Request(
- url,
- method="DELETE",
- headers={"AccessKey": access_key},
- )
- try:
- with urllib.request.urlopen(req, timeout=timeout) as resp:
- code = resp.getcode()
- except urllib.error.HTTPError as e:
- code = e.code
- if code == 404:
- return
- e.read(500)
- raise SystemExit(f"HTTP {code} DELETE {url}") from e
- else:
- if code not in (200, 201, 204):
- raise SystemExit(f"unexpected DELETE status {code} for {url}")
-
-
-def prune_other_versions(
- base: str,
- access_key: str,
- track: str,
- keep_version: str,
-) -> None:
- """Remove every version directory under ``track/`` except ``keep_version``."""
- base = base.rstrip("/")
- list_url = f"{base}/{encode_object_rel(track)}/"
- try:
- listing = get_json(list_url, access_key)
- except urllib.error.HTTPError as e:
- if e.code == 404:
- return
- raise
- if not isinstance(listing, list):
- return
- seen: set[str] = set()
- for item in listing:
- if not isinstance(item, dict):
- continue
- if not item.get("IsDirectory"):
- continue
- name = item.get("ObjectName")
- if not name or not isinstance(name, str):
- continue
- if name in seen:
- continue
- seen.add(name)
- if name == keep_version:
- continue
- rel = f"{track}/{name}"
- delete_url = f"{base}/{encode_object_rel(rel)}"
- print(f"prune: DELETE {delete_url}", file=sys.stderr)
- delete_path(delete_url, access_key)
-
-
-def put_file(
- url: str,
- body: bytes,
- access_key: str,
- content_type: str,
- max_attempts: int = 4,
-) -> None:
- checksum = hashlib.sha256(body).hexdigest().upper()
- timeout = 600 if len(body) > 50_000_000 else 120
- for attempt in range(1, max_attempts + 1):
- req = urllib.request.Request(
- url,
- data=body,
- method="PUT",
- headers={
- "AccessKey": access_key,
- "Content-Type": content_type,
- "Checksum": checksum,
- },
- )
- try:
- with urllib.request.urlopen(req, timeout=timeout) as resp:
- code = resp.getcode()
- except urllib.error.HTTPError as e:
- code = e.code
- err_body = e.read(500)
- if code in (200, 201):
- return
- if 500 <= code < 600 and attempt < max_attempts:
- time.sleep(0.5 * (2 ** (attempt - 1)))
- continue
- raise SystemExit(
- f"HTTP {code} for {url}: {err_body!r}",
- ) from e
- except (urllib.error.URLError, TimeoutError) as e:
- if attempt < max_attempts:
- time.sleep(0.5 * (2 ** (attempt - 1)))
- continue
- raise SystemExit(f"request failed for {url}: {e}") from e
- else:
- if code in (200, 201):
- return
- raise SystemExit(f"unexpected status {code} for {url}")
-
-
-def main() -> None:
- base = os.environ.get("BUNNY_STORAGE_BASE_URL", "").rstrip("/")
- key = os.environ.get("BUNNY_STORAGE_ACCESS_KEY", "")
- prefix = os.environ.get("BUNNY_STORAGE_OBJECT_PREFIX", "").strip("/")
- if not base or not key:
- print(
- "BUNNY_STORAGE_BASE_URL and BUNNY_STORAGE_ACCESS_KEY must be set",
- file=sys.stderr,
- )
- sys.exit(1)
- if prefix:
- seg = prefix.split("/", 1)
- if len(seg) == 2 and seg[0] in ("master", "dev"):
- prune_other_versions(base, key, seg[0], seg[1])
- root = Path(sys.argv[1]).resolve()
- if not root.is_dir():
- print(f"not a directory: {root}", file=sys.stderr)
- sys.exit(1)
-
- files = sorted(p for p in root.rglob("*") if p.is_file())
- if not files:
- print(f"no files under {root}", file=sys.stderr)
- sys.exit(1)
-
- for path in files:
- rel = path.relative_to(root).as_posix()
- if prefix:
- object_rel = f"{prefix}/{rel}"
- else:
- object_rel = rel
- url = f"{base}/{encode_object_rel(object_rel)}"
- body = path.read_bytes()
- put_file(url, body, key, mime_for(path))
- print(url)
-
-
-if __name__ == "__main__":
- main()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────